如何避免ASP.NET Core与EF Core中的实体跟踪异常
异常信息
实体类型'Airline'的实例无法被跟踪,因为已有键值为'{CarrierCode: DY}'的实例正在被跟踪。附加现有实体时,请确保每个键值仅附加一个实体实例。
触发场景
该异常出现在PassengerController的AddBaggage()方法中foreach循环的第二次迭代,触发代码行:
await _baggageRepository.AddAsync(newBaggage);
相关代码片段
PassengerController.AddBaggage方法
[HttpPost("{id}/flight/{flightId}/add-baggage")] public async Task<ActionResult<Baggage>> AddBaggage(Guid id, int flightId, [FromBody] List<AddBaggageModel> addBaggageModels) { var passenger = await _passengerRepository.GetPassengerByIdAsync(id); var selectedFlight = await _flightRepository.GetFlightByIdAsync(flightId); var destination = await _destinationRepository.GetDestinationByCriteriaAsync(f => f.IATAAirportCode == addBaggageModels.FirstOrDefault().FinalDestination); var baggageList = new List<Baggage>(); foreach (var baggageModel in addBaggageModels) { var newBaggage = new Baggage { PassengerId = passenger.Id, Weight = baggageModel.Weight, DestinationId = destination.IATAAirportCode }; //...some code else if (baggageModel.TagType == TagTypeEnum.System) { var number = _baggageRepository.GetNextSequenceValue("BaggageTagsSequence"); var baggageTag = new BaggageTag(selectedFlight.ScheduledFlight.Airline, number); newBaggage.BaggageTag = baggageTag; } await _baggageRepository.AddAsync(newBaggage); // 异常触发点:第二次迭代时重复引用同一Airline实例 baggageList.Add(newBaggage); var orderedFlights = passenger.Flights .Where(f => f.Flight.DepartureDateTime >= selectedFlight.DepartureDateTime) .OrderBy(f => f.Flight.DepartureDateTime) .ToList(); foreach (var connectingFlight in orderedFlights) { //...some code FlightBaggage flightBaggage = new FlightBaggage { Flight = connectingFlight.Flight, Baggage = newBaggage, BaggageType = baggageType }; newBaggage.Flights.Add(flightBaggage); //...some code } } await _baggageRepository.UpdateAsync(baggageList.ToArray()); return Ok(); }
BaggageTag构造函数
public BaggageTag(Airline? airline, int number) { Airline = airline; LeadingDigit = 0; Number = number; TagNumber = _GetBaggageTagNumber(); TagType = TagTypeEnum.System; }
FlightRepository.GetFlightByIdAsync方法
public async Task<Flight> GetFlightByIdAsync(int id) { var CACHE_KEY = $"Flight_{id}"; if (!_cache.TryGetValue(CACHE_KEY, out Flight flight)) { flight = await _context.Flights.AsNoTracking() .Include(_ => _.ScheduledFlight) .ThenInclude(_ => _.DestinationFrom) .Include(_ => _.ScheduledFlight) .ThenInclude(_ => _.DestinationTo) .Include(_ => _.ScheduledFlight) .ThenInclude(_ => _.Airline) .Include(_ => _.Aircraft) .ThenInclude(_ => _.AircraftType) .Include(_ => _.Boarding) .Include(_ => _.ListOfBookedPassengers) .ThenInclude(_ => _.Passenger) .Include(_ => _.ListOfCheckedBaggage) .ThenInclude(_ => _.Baggage) .Include(_ => _.Seats) .FirstOrDefaultAsync(_ => _.Id == id); if (flight != null) { _cache.Set(CACHE_KEY, flight, new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(10) }); } } return flight; }
PassengerRepository.GetPassengerByIdAsync方法
public async Task<Passenger> GetPassengerByIdAsync(Guid id) { return await _context.Passengers.AsNoTracking() .Include(_ => _.PNR) .Include(_ => _.FrequentFlyer) .Include(_ => _.TravelDocuments) .Include(_ => _.PassengerCheckedBags) .ThenInclude(_ => _.BaggageTag) .Include(_ => _.PassengerCheckedBags) .ThenInclude(_ => _.SpecialBag) .Include(_ => _.Flights) .ThenInclude(_ => _.Flight) .ThenInclude(_ => _.ScheduledFlight) .ThenInclude(_ => _.Airline) .Include(_ => _.Comments) .ThenInclude(_ => _.PredefinedComment) .Include(_ => _.AssignedSeats) .Include(_ => _.SpecialServiceRequests) .ThenInclude(_ => _.SSRCode) .FirstOrDefaultAsync(_ => _.Id == id); }
问题分析
使用AsNoTracking()查询出的Airline实体,在第一次循环中通过BaggageTag关联到Baggage并被添加到上下文后,ChangeTracker开始跟踪该Airline实例;第二次循环时,再次使用同一个未被跟踪的Airline实例关联新的BaggageTag,尝试添加时就会触发跟踪冲突——因为上下文已经存在一个同键的Airline实例在被跟踪。
移除AsNoTracking()能解决问题,但会增加内存开销,因此需要在保留优化的前提下修复异常。
解决方案
方案1:关联主键而非整个实体(推荐)
修改BaggageTag的构造逻辑,不直接传递Airline实体,而是传入其主键(如CarrierCode),通过外键关联而非实体引用:
// 修改BaggageTag构造函数 public BaggageTag(string airlineCarrierCode, int number) { AirlineCarrierCode = airlineCarrierCode; // 假设这是Airline实体的外键属性 LeadingDigit = 0; Number = number; TagNumber = _GetBaggageTagNumber(); TagType = TagTypeEnum.System; } // 在Controller循环内调用 var baggageTag = new BaggageTag(selectedFlight.ScheduledFlight.Airline.CarrierCode, number);
如果实体没有显式外键属性,可在循环外先获取或附加Airline到上下文,确保循环内使用同一个已跟踪实例:
// 循环外提前处理Airline var airlineCarrierCode = selectedFlight.ScheduledFlight.Airline.CarrierCode; var trackedAirline = await _context.Airlines.FindAsync(airlineCarrierCode); if (trackedAirline == null) { trackedAirline = selectedFlight.ScheduledFlight.Airline; _context.Airlines.Attach(trackedAirline); } // 循环内使用该实例 var baggageTag = new BaggageTag(trackedAirline, number);
方案2:在仓储Add方法中检查跟踪状态
在通用仓储的AddAsync方法中,提前检查关联的Airline是否已被跟踪,替换为已跟踪实例或附加未跟踪实例:
// GenericRepository.AddAsync示例 public async Task AddAsync<T>(T entity) where T : class { if (entity is Baggage baggage && baggage.BaggageTag?.Airline != null) { var existingAirline = await _context.Airlines.FindAsync(baggage.BaggageTag.Airline.CarrierCode); if (existingAirline != null) { // 用上下文已跟踪的实例替换未跟踪的 baggage.BaggageTag.Airline = existingAirline; } else { // 附加未跟踪的Airline实例 _context.Airlines.Attach(baggage.BaggageTag.Airline); } } await _context.Set<T>().AddAsync(entity); }
方案3:分离查询后的Airline实例(不推荐)
如果必须保留原实体传递方式,可在查询后立即分离Airline,但可能引发其他跟踪问题:
// 在FlightRepository.GetFlightByIdAsync中,查询后分离Airline if (flight != null) { _context.Entry(flight.ScheduledFlight.Airline).State = EntityState.Detached; _cache.Set(CACHE_KEY, flight, new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(10) }); }
额外优化建议
AddBaggage方法中循环内多次调用AddAsync,最后又调用UpdateAsync,存在冗余操作。可以将所有Baggage收集到列表后一次性添加,减少上下文跟踪压力:
// 移除循环内的await _baggageRepository.AddAsync(newBaggage); baggageList.Add(newBaggage); // 循环结束后批量添加 await _baggageRepository.AddRangeAsync(baggageList); // 无需再调用UpdateAsync,AddRange已标记实体为Added状态 await _context.SaveChangesAsync();
内容的提问来源于stack exchange,提问作者Maicuss

