I'm encountering an issue in my ASP.NET Core application with Entity Framework Core where I repeatedly get the following response:
The instance of entity type 'Airline' cannot be tracked because another instance with the key value '{CarrierCode: DY}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
This occurs during the second iteration of a foreach loop in the AddBaggage() method in my PassengerController on the following line of code:
await _baggageRepository.AddAsync(newBaggage);
Here's the method:
[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); // An exception is thrown here, on the second iteration, when we attempt to assign a reference to the same object/instance of Airline as in the first iteration in the constructor parameter.
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();
}
Here is also one of the BaggageTag constructors:
public BaggageTag(Airline? airline, int number)
{
Airline = airline;
LeadingDigit = 0;
Number = number;
TagNumber = _GetBaggageTagNumber();
TagType = TagTypeEnum.System;
}
And I also have the following code in my PassengerRepository and FlightRepository:
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;
}
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);
}
I understand that the issue stems from the ChangeTracker attempting to attach an entity that is already being tracked in the context.
However, I'm unsure how to efficiently avoid this. I've read about detaching entities using
dbContext.Entry(entity).State = EntityState.Detached
but I'm unsure where to place this line of code.
Should I add it in the PassengerRepository, FlightRepository, or GenericRepository within methods like AddAsync(), UpdateAsync() and DeleteAsync()?
Alternatively, should I override the SaveChangesAsync() method in my AppDbContext and include it there?
I'd appreciate any guidance or alternative solutions to resolve this issue effectively. Thank you for your help!
I've tried to remove all AsNoTracking() and AsQueryable() methods from my repositories, and everything worked perfectly. However, I initially added these methods to reduce memory overhead, and since then, I've been experiencing this problem.
A few issues come to mind but the main suspect is that your repository methods are fetching data using
AsNoTracking()(I.e. selectedFlight.ScheduledFlight.Airline) yet you are trying to associate these to a new instance of this booking and associated properties. When loading entities to be associated you do not want to useAsNoTracking()as this will return a fresh, and detached instance of entities when theDbContextmay already be tracking other instances. This can result in errors like what you are seeing, or cases where you get exceptions due to index constraints or it inserts a duplicate record with a new ID. (it attempts to insert an associated untracked instance) Removing theAsNoTracking()in the fetching queries will likely resolve the issue.This is one reason I do not recommend using Repository pattern like this because every call via the repository is a separate concern for that repository method. Some calls may want related data, some may not need it. Loading everything can be expensive so a "quick fix" might be to tack on an
AsNoTracking()to improve performance, but that leads to unexpected problems where other code like updates like this would expect to be dealing with tracked references. If code uses the in-build repository pattern (DbSet) then consumers can better manage what related data to include and optimizeAsNoTracking()for read-only operations.