I am new to Blazor and EF world but have reasonable experience with other frameworks such as django and laravel. I am trying to wrap my head around attaching the entities to a db context. It is said that IDbContextFactory is better than using DBContext due to its scope and I agree. However, I believe managing navigational properties and the amount of work that needs to be done to determine if entity is tracked or not seems to be counterproductive. Surely, I might be doing it wrong!Please do point if you identify anything I can improve
Current issue is that when I have two products or more setting EntityState.Unchanged the second time throws exception
System.InvalidOperationException: The instance of entity type 'ProductType' cannot be tracked because another instance with the key value '{Id: 2}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.Repository
public class JobRepository : IJobRepository { private readonly IDbContextFactory<ApplicationDbContext> _contextFactory; private readonly IUserService _userService; public JobRepository(IDbContextFactory<ApplicationDbContext> contextFactory, IUserService userService) { _contextFactory = contextFactory; _userService = userService; } public async Task AddJobAsync(Job job) { if (job == null) { throw new ArgumentNullException(nameof(job)); } try { using var _context = _contextFactory.CreateDbContext(); var user = await _userService.GetLoggedInUserAsync(); job.CreatedById = user.Id; job.CustomerId = user.CustomerId.GetValueOrDefault(); if (job.Address != null) { //_context.Attach(job.Address); _context.Entry(job.Address).State = EntityState.Unchanged; } if(job.Site != null) { //_context.Attach(job.Site); _context.Entry(job.Site).State = EntityState.Unchanged; } if (job.CustomerId > 0) { job.Customer = await _context.Customers.FindAsync(job.CustomerId); if (job.Customer != null) { //_context.Attach(job.Customer); _context.Entry(job.Customer).State = EntityState.Unchanged; } } if (job.Insurer != null) { //_context.Attach(job.Insurer); _context.Entry(job.Insurer).State = EntityState.Unchanged; } foreach (var product in job.JobProducts) { if (product.ProductId > 0 && product.Product!=null) { _context.Entry(product.Product).State = EntityState.Unchanged; if (product.Product.ProductType != null) { _context.Entry(product.Product.ProductType).State = EntityState.Unchanged; } } } job.CreatedAt = DateTime.Now; _context.Jobs.Add(job); await _context.SaveChangesAsync(); } catch (Exception ex) { throw ex; } }}Service that uses automapper
public async Task CreateJobRequestAsync(JobDTO job) { var jobEntity = _mapper.Map<Job>(job); await _jobRepository.AddJobAsync(jobEntity); }JobDTO
namespace Shared.DTOs.Common.Jobs{ public class JobDTO { // Properties public int Id { get; set; } public DateTime CreatedAt { get; set; } public DateTime? EarliestBy { get; set; } public DateTime? LatestBy { get; set; } public DeliveryPreference DeliveryPreference { get; set; } public DeliveryTime DeliveryTime { get; set; } public string? Instructions { get; set; } public string? PurchaseOrderNumber { get; set; } public string? CheckoutSessionId { get; set; } // Relations public int? AddressId { get; set; } public AddressDTO? Address { get; set; } public int? SiteID { get; set; } public ProjectSiteDTO? Site { get; set; } public string CreatedById { get; set; } = default!; public ApplicationUserDTO CreatedBy { get; set; } = default!; public int CustomerId { get; set; } public CustomerDTO? Customer { get; set; } public int? InsurerId { get; set; } public InsurerDTO? Insurer { get; set; } public List<JobLifeCycleDTO> LifeCycleEntries { get; set; } = new List<JobLifeCycleDTO>(); public List<JobInviteDTO> Invites { get; set; } = new List<JobInviteDTO>(); public List<JobProductDTO> JobProducts { get; set; } = new List<JobProductDTO>(); }}Model Job
namespace Shared.Entities.Common.Jobs{ public class Job { //Properties public int Id { get; set; } public DateTime CreatedAt { get; set; } public DateTime? EarliestBy { get; set; } public DateTime? LatestBy { get; set; } public DeliveryPreference DeliveryPreference { get; set; } public DeliveryTime DeliveryTime { get; set; } public string? Instructions { get; set; } public string? PurchaseOrderNumber { get; set; } public string? CheckoutSessionId { get; set; } //Relations public int? AddressId { get; set; } [ForeignKey("AddressId")] public Address? Address { get; set; } public int? SiteID { get; set; } [ForeignKey("SiteID")] public ProjectSite? Site { get; set; } [Required] public string CreatedById { get; set; } [ForeignKey("CreatedById")] public ApplicationUser CreatedBy { get; set; } public int CustomerId { get; set; } [ForeignKey("CustomerId")] public Customer? Customer { get; set; } public int? InsurerId { get; set; } [ForeignKey("InsurerId")] public Insurer? Insurer { get; set; } public virtual ICollection<JobLifeCycle> LifeCycleEntries { get; set; } = new List<JobLifeCycle>(); public virtual List<JobInvite> Invites { get; set; } = new List<JobInvite>(); public virtual ICollection<JobProduct> JobProducts { get; set; } = new HashSet<JobProduct>(); // public JobLifeCycle? LatestStatus => LifeCycleEntries.OrderByDescending(e => e.ActionedAt).FirstOrDefault(); }}