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.
Related
I'm trying to store older versions of entities in my database. To do that I am copying the existing values before I update them. For some reason EF Core won't let me use the same batch.Values property twice.
public async Task<Batch> UpdateBatch(Batch batch, Batch updatedBatch)
{
foreach (var valueParameter in batch.Values)
{
batch.ValuesHistory.Add(new ParameterValueHistory
{
Parameter = valueParameter.Parameter,
ParameterBatchNumber = valueParameter.ParameterBatchNumber,
Value = valueParameter.Value
});
}
batch.Values = updatedBatch.Values;
batch.Version++;
await this.context.SaveChangesAsync();
return batch;
}
The foreach loop and batch.Values = updatedBatch.Values; work exactly like they should when only one of them exists. But whenever they're both active I get the following error:
The instance of entity type 'ParameterValue' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
These are the relevant models:
ParameterValue:
public class ParameterValue
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
public virtual RecipeParameter Parameter { get; set; }
public string Value { get; set; }
public string? ParameterBatchNumber { get; set; }
}
ParameterValueHistory:
public class ParameterValueHistory
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
public virtual RecipeParameter Parameter { get; set; }
public string Value { get; set; }
public string? ParameterBatchNumber { get; set; }
}
RecipeParameter for context:
public class RecipeParameter
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Type { get; set; }
public string Unit { get; set; }
public string Value { get; set; }
public bool BatchRequired { get; set; }
}
Batch:
public class Batch
{
[Key]
[MaxLength(12)]
public string BatchNumber { get; set; }
public virtual List<ParameterValue> Values { get; set; }
public virtual List<ParameterValueHistory> ValuesHistory { get; set; }
public int Version { get; set; }
[Required]
public bool IsResearch { get; set; }
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime CreatedOn { get; set; } = DateTime.UtcNow;
}
This is my DbContext class:
public class ApplicationDataContext : DbContext
{
public ApplicationDataContext(DbContextOptions<ApplicationDataContext> options)
: base(options)
{
}
public DbSet<Product> Product { get; set; }
public DbSet<Batch> Batch { get; set; }
public DbSet<ParameterValue> ParameterValue { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLazyLoadingProxies();
base.OnConfiguring(optionsBuilder);
}
}
Why does this error keep showing up? Even when I am just accessing the propety as batch.Values more than once, it gives me this error.
UPDATE:
This is the controller method that calls the UpdateBatch method.
[HttpPut("{productId}/batches/{batchNumber}")]
public async Task<ActionResult<Batch>> PutBatch(string batchNumber, Batch updatedBatch)
{
Batch batch = await this.repository.GetBatchByBatchNumber(batchNumber);
if (batch == null)
{
return NotFound()
}
return await this.repository.UpdateBatch(batch, updatedBatch);
}
When you use batch.Values = updatedBatch.Values;, because batch.Values contains the foreign key of Batch, and if the value in updatedBatch.Values also contains the key value,if the equal operation is performed directly, due to the foreign key constraint, the foreign key cannot be modified directly, which will cause your error.
Therefore, you cannot include the key value in the Values in your updateBatch.
Regarding your question. I did a simple test. You can see the following code(updateBatch.Values have no Id).
var batch = _context.Batches.Include(c => c.Values)
.ThenInclude(c => c.Parameter)
.Include(b => b.ValuesHistory)
.ThenInclude(c => c.Parameter)
.Where(c => c.BatchNumber == "1")
.FirstOrDefault();
var updateBatch = new Batch
{
Version = 3,
CreatedOn = new DateTime(),
IsResearch = true,
Values = new List<ParameterValue>
{
new ParameterValue
{
Value = "hello",
Parameter = new RecipeParameter
{
BatchRequired = true,
Name = "h",
Type = "e",
Unit = "l",
Value = "o"
}
},
},
ValuesHistory = new List<ParameterValueHistory>()
};
foreach (var valueParameter in batch.Values)
{
batch.ValuesHistory.Add(new ParameterValueHistory
{
Parameter = valueParameter.Parameter,
ParameterBatchNumber = valueParameter.ParameterBatchNumber,
Value = valueParameter.Value
});
}
batch.Values = updateBatch.Values;
batch.Version++;
_context.SaveChanges();
Test result:
start by making these changes..
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
should not be on
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime CreatedOn { get; set; } = DateTime.UtcNow;
instead model like
public class Batch
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
//you can add index on this
[MaxLength(12)]
public string BatchNumber { get; set; }
public int Version { get; set; }
[Required]
public bool IsResearch { get; set; }
[Required]
public DateTime CreatedOn { get; set; };// set this in the repo or create do another way
//you add this but don't see the linkage aka ParameterValue does not have a BatchId
public virtual List<ParameterValue> Values { get; set; }
public virtual List<ParameterValueHistory> ValuesHistory { get; set; }
}
I am new to EF and .Net core and I'm having trouble with many-to-many relationship in my project. I used microsoft documentation to setup the relationship, but i have trouble inserting any data. Project is a kanban board and i am trying to set up relations between users and tasks. Both of them already exist. The goal is to have a table with userId and taskId. Here are my models:
KanbanTask Model:
public class KanbanTask : Entity
{
public string Title { get; set; }
[Required]
public string Description { get; set; }
[Required]
public string Status { get; set; }
public int ProgressStatus { get; set; }
public List<UserTask> UserTask { get; set; }
}
User Model:
public class User : Entity
{
[Required]
public string Name { get; set; }
[Required]
public string Surname { get; set; }
public List<UserTask> UserTask { get; set; }
}
Entity Model:
public class Entity
{
public int Id { get; set; }
}
UserTaskModel:
public class UserTask
{
public int UserId { get; set; }
public User User { get; set; }
public int KanbanTaskId { get; set; }
public KanbanTask KanbanTask { get; set; }
}
My DbContex:
public DbSet<KanbanTask> KanbanTasks { get; set; }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserTask>()
.HasKey(t => new { t.UserId, t.KanbanTaskId });
modelBuilder.Entity<UserTask>()
.HasOne(pt => pt.User)
.WithMany(p => p.UserTask)
.HasForeignKey(pt => pt.UserId);
modelBuilder.Entity<UserTask>()
.HasOne(pt => pt.KanbanTask)
.WithMany(t => t.UserTask)
.HasForeignKey(pt => pt.KanbanTaskId);
}
}
My function in service:
public async Task<ResultDTO> AssignTaskToUser(int taskId, int userId)
{
var result = new ResultDTO()
{
Response = null
};
try
{
var user = await _repo.GetSingleEntity(x => x.Id == userId);
var kanbanTask = await _taskrepo.GetSingleEntity(y => y.Id == taskId);
if (user != null && kanbanTask != null)
{
var usertask = new UserTask()
{
KanbanTaskId = taskId,
UserId = userId
};
kanbanTask.UserTask.Add(usertask);
user.UserTask.Add(usertask);
await _repo.Patch(user);
}
else
result.Response = "Task or user not found";
}
catch (Exception e)
{
result.Response = e.Message;
return result;
}
return result;
}
My repository:
public async Task Patch(T entity)
{
_dbSet.Update(entity);
await _context.SaveChangesAsync();
}
Like this
var usertask = new UserTask()
{
KanbanTaskId = taskId,
UserId = userId
};
db.UserTasks.Add(usertask);
db.SaveChanges();
What you need to is to make sure that your middle entity (UserTask) always saves the Keys of both entities so I strongly suggest to add that logic in UserTask constructor.
public class UserTask
{
public int UserId { get; set; }
public User User { get; set; }
public int KanbanTaskId { get; set; }
public KanbanTask KanbanTask { get; set; }
public UserTask() { }
public UserTask(User user, KanbanTask kanbanTask)
{
KanbanTask = kanbanTask;
KanbanTaskId = kanbanTask.Id;
User = user;
UserId = userId;
}
}
//
var usertask = new UserTask(user, kanbanTask);
kanbanTask.UserTask.Add(usertask);
user.UserTask.Add(usertask);
await _repo.Patch(user);
//
I have wrote an example for this common problem. Here https://github.com/salsita18/ManyToManyNetCore you can check the approach I took, using a single generic MiddleEntity class.
I also added it to nuget in order to reuse it, but you can just make your own implementation following the pattern
I have 3 Models, 3 tables in DB (created with EF migrations):
public class Announce {
public int Id { get; set; }
public Location Location { get; set;}
public int LocationId { get; set; }
}
public class Location {
public int Id { get; set; }
public string Name { get; set; }
public District District { get; set; }
public string DistrictId { get; set; }
}
public class District {
public int Id { get; set; }
public string Name { get; set; }
}
and a Dto class :
public class AnnounceForListDto {
public int Id { get; set; }
public string LocationName { get; set; }
public string DistrictName{ get; set; }
}
And in AutoMapperProfile :
CreateMap<District, AnnounceForListDto>()
.ForMember(dest => dest.DistrictName, opt =>
{
opt.MapFrom(dis => dis.Name);
});
And I want to getAnnounces as :
public async Task<IEnumerable<Announce>> GetAnnounces()
{
var announces = await _context.Announce
.Include(prop => prop.Photos)
.Include(prop => prop.Location)
.ToListAsync();
return announces;
}
I need to include the District Name (or District Id, because I use AutoMapper to equalise DistrictName from AnnounceForListDto with District.Name) in my list.
I tried something as .Include(prop => prop.Location.District.Name), but get an error, "Include can use only one "dot").
Maybe .ThenInclude(Location => Location.District) this help and I wrong in my Dto declaration?
My Dto is used in Controller :
[HttpGet]
public async Task<IActionResult> GetAnnounces()
{
var announces = await _repo.GetAnnounces();
var announcesToReturn = _mapper.Map<IEnumerable<AnnounceForListDto>>(announces);
return Ok(announcesToReturn);
}
Solved :
Create relation between Location and District
then, my Repo method :
var announces = await _context.Announce
.Include(prop => prop.Photos)
.Include(prop => prop.Location)
.ThenInclude(prop => prop.District)
.ToListAsync();
return announces;
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);
}
}
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.