C
C#2mo ago
Gax

Deleting an entity with a many-to-many relationship throws an ArgumentNullException

I have a Memory and Person tables, with a junction table MemoryPerson that represents a many-to-many relationship between them The expected behaviour I want is to delete any related MemoryPerson whenever a Memory or Person is deleted. I have repositories for each entity, and they all follow the same general implementation:
public async Task DeleteAsync(Memory entity)
{
_context.Memories.Remove(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(Memory entity)
{
_context.Memories.Remove(entity);
await _context.SaveChangesAsync();
}
That said, here comes my issue: When deleting a Person, all related MemoryPerson get deleted as expected, however when doing the same for Memory, an ArgumentNullException is thrown with the message Value cannot be null. (Parameter 'key'). The Call Stack points to that exact DeleteAsync method. I have checked and none of the values of the entity passed are null, I also tried adding checks in each of the methods but to no success. Does anyone have any idea of a possible solution? I'll send the OnConfiguring override in the replies due to character limit. A few extra details: - The entities are created properly - Deleting a memory without an existing MemoryPerson does not throw an exception - Deleting a memory with a MemoryPerson that does not relate to said memory does not throw an exception
27 Replies
Gax
GaxOP2mo ago
It is currently configured this way: (I apologize for broken indentation)
modelBuilder.Entity<Person>(entity =>
{
entity.HasKey(p => p.Id); // Primary key
entity.Property(p => p.FullName).IsRequired().HasMaxLength(200);
entity.Property(p => p.Bio).HasMaxLength(1000);
entity.Property(p => p.ProfilePictureUrl).HasMaxLength(500);
entity.Property(p => p.Birthday).HasColumnType("datetime");

// Relationship with MemoryPerson
entity.HasMany(p => p.MemoryPeople)
.WithOne(mp => mp.Person)
.HasForeignKey(mp => mp.PersonId)
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity<Memory>(entity =>
{
entity.HasKey(m => m.Id); // Primary key
entity.Property(m => m.Title).IsRequired().HasMaxLength(200);
entity.Property(m => m.Description).HasMaxLength(2000);
entity.Property(m => m.Date).HasColumnType("datetime");

// Relationship with MemoryPerson
entity.HasMany(m => m.MemoryPeople)
.WithOne(mp => mp.Memory)
.HasForeignKey(mp => mp.MemoryId)
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity<MemoryPerson>(entity =>
{
entity.HasKey(mp => new { mp.MemoryId, mp.PersonId }); // Composite key

// Relationship with Person
entity.HasOne(mp => mp.Person)
.WithMany(p => p.MemoryPeople)
.HasForeignKey(mp => mp.PersonId);

// Relationship with Memory
entity.HasOne(mp => mp.Memory)
.WithMany(m => m.MemoryPeople)
.HasForeignKey(mp => mp.MemoryId);
});
modelBuilder.Entity<Person>(entity =>
{
entity.HasKey(p => p.Id); // Primary key
entity.Property(p => p.FullName).IsRequired().HasMaxLength(200);
entity.Property(p => p.Bio).HasMaxLength(1000);
entity.Property(p => p.ProfilePictureUrl).HasMaxLength(500);
entity.Property(p => p.Birthday).HasColumnType("datetime");

// Relationship with MemoryPerson
entity.HasMany(p => p.MemoryPeople)
.WithOne(mp => mp.Person)
.HasForeignKey(mp => mp.PersonId)
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity<Memory>(entity =>
{
entity.HasKey(m => m.Id); // Primary key
entity.Property(m => m.Title).IsRequired().HasMaxLength(200);
entity.Property(m => m.Description).HasMaxLength(2000);
entity.Property(m => m.Date).HasColumnType("datetime");

// Relationship with MemoryPerson
entity.HasMany(m => m.MemoryPeople)
.WithOne(mp => mp.Memory)
.HasForeignKey(mp => mp.MemoryId)
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity<MemoryPerson>(entity =>
{
entity.HasKey(mp => new { mp.MemoryId, mp.PersonId }); // Composite key

// Relationship with Person
entity.HasOne(mp => mp.Person)
.WithMany(p => p.MemoryPeople)
.HasForeignKey(mp => mp.PersonId);

// Relationship with Memory
entity.HasOne(mp => mp.Memory)
.WithMany(m => m.MemoryPeople)
.HasForeignKey(mp => mp.MemoryId);
});
A simple recreation for the problem is just
await personRepo.AddAsync(person);
await memoryRepo.AddAsync(memory);

var memoryPerson = new MemoryPerson
{
MemoryId = memory.Id,
PersonId = person.Id,
};

await memoryPersonRepo.AddAsync(memoryPerson);
await memoryRepo.DeleteAsync(memory);
await personRepo.AddAsync(person);
await memoryRepo.AddAsync(memory);

var memoryPerson = new MemoryPerson
{
MemoryId = memory.Id,
PersonId = person.Id,
};

await memoryPersonRepo.AddAsync(memoryPerson);
await memoryRepo.DeleteAsync(memory);
@yoajdin
@yoajdin2mo ago
Is this an issue with pasting? You're missing a ");" at the end below. Not sure if it will resolve your issue it's just the first thing that stepped out to me modelBuilder.Entity<Memory>(entity => { entity.HasKey(m => m.Id); // Primary key entity.Property(m => m.Title).IsRequired().HasMaxLength(200); entity.Property(m => m.Description).HasMaxLength(2000); entity.Property(m => m.Date).HasColumnType("datetime"); // Relationship with MemoryPerson entity.HasMany(m => m.MemoryPeople) .WithOne(mp => mp.Memory) .HasForeignKey(mp => mp.MemoryId) .OnDelete(DeleteBehavior.Cascade); }
Gax
GaxOP2mo ago
yeah it was an issue with pasting, my bad the ); is present in the code, just was cut out when copying the source code
Present Chozo
Present Chozo2mo ago
Your problem is how you're declaring your MemoryPerson, I think. Try declaring it like this:
var memoryPerson = new MemoryPerson()
{
Memory = memory,
Person = person
};
var memoryPerson = new MemoryPerson()
{
Memory = memory,
Person = person
};
Gax
GaxOP2mo ago
same error
Present Chozo
Present Chozo2mo ago
Do you have a stack trace? ...well, one you can publically share, I guess.
Gax
GaxOP2mo ago
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'key')
at System.Collections.Generic.Dictionary`2.FindValue(TKey key)
at System.Collections.Generic.Dictionary`2.TryGetValue(TKey key, TValue& value)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityReferenceMap.TryGet(Object entity, IEntityType entityType, InternalEntityEntry& entry, Boolean throwOnNonUniqueness)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.TryGetEntry(Object entity, IEntityType entityType, Boolean throwOnTypeMismatch)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.GetOrCreateEntry(Object entity, IEntityType entityType)
<a few more frames from EFCore related to detecting changes and delete>
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Meminisse.Application.Repositories.MemoryRepository.DeleteAsync(Memory entity) in C:\Users\<redacted>\source\repos\Meminisse\Meminisse.Application\Repositories\MemoryRepository.cs:line 30
at Program.<Main>$(String[] args) in C:\Users\<redacted>\source\repos\Meminisse\Meminisse.TestApp\Program.cs:line 45
at Program.<Main>(String[] args)
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'key')
at System.Collections.Generic.Dictionary`2.FindValue(TKey key)
at System.Collections.Generic.Dictionary`2.TryGetValue(TKey key, TValue& value)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityReferenceMap.TryGet(Object entity, IEntityType entityType, InternalEntityEntry& entry, Boolean throwOnNonUniqueness)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.TryGetEntry(Object entity, IEntityType entityType, Boolean throwOnTypeMismatch)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.GetOrCreateEntry(Object entity, IEntityType entityType)
<a few more frames from EFCore related to detecting changes and delete>
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
at Meminisse.Application.Repositories.MemoryRepository.DeleteAsync(Memory entity) in C:\Users\<redacted>\source\repos\Meminisse\Meminisse.Application\Repositories\MemoryRepository.cs:line 30
at Program.<Main>$(String[] args) in C:\Users\<redacted>\source\repos\Meminisse\Meminisse.TestApp\Program.cs:line 45
at Program.<Main>(String[] args)
i cant send full stack trace due to character limit but i can probably put on a pastebin if needed
Present Chozo
Present Chozo2mo ago
Sure. But even from that excerpt it seems like something didn't save correctly.
Gax
GaxOP2mo ago
i thought about that but everything is being added to the database correctly the memory, person and memoryperson are all saved in the database
Present Chozo
Present Chozo2mo ago
I'm guessing the Memory object also has its Id populated by the time you try and delete it, right?
Gax
GaxOP2mo ago
yep
Present Chozo
Present Chozo2mo ago
And do you know for sure that your change tracker is tracking that entity when you try and delete it? Or I guess a better question is "Is the DbContext you're using to delete the entity the same one you used to add it?"
Gax
GaxOP2mo ago
it is, yes
Present Chozo
Present Chozo2mo ago
Hmmm... If you're using a repo pattern, I assume you have a Get method, right?
Gax
GaxOP2mo ago
i do
Present Chozo
Present Chozo2mo ago
Are you using FindAsync() or FirstOrDefault()?
Gax
GaxOP2mo ago
FindAsync
Present Chozo
Present Chozo2mo ago
Try getting getting the most up-to-date instance of that Memory object prior to deleting it. If the change tracker is already tracking it, memory = await memoryRepo.GetAsync(memory.Id) shouldn't call the DB. See if that also throws an error.
Gax
GaxOP2mo ago
same exception what bugs me is that whatever i do with Memory works perfectly with Person, they call the "same" methods (from their respective tables), have pretty much the same implementation and only one of them has this issue
Present Chozo
Present Chozo2mo ago
Maybe show both of the repo classes for both entities?
Gax
GaxOP2mo ago
public class MemoryRepository : IRepository<Memory>
{
private readonly MeminisseDbContext _context;

public MemoryRepository(MeminisseDbContext context)
{
_context = context;
}

public async Task AddAsync(Memory entity)
{
await _context.Memories.AddAsync(entity);
await _context.SaveChangesAsync();
}

public async Task AddRangeAsync(List<Memory> entities)
{
await _context.Memories.AddRangeAsync(entities);
await _context.SaveChangesAsync();
}

public async Task DeleteAsync(Memory entity)
{
_context.Memories.Remove(entity);
await _context.SaveChangesAsync();
}

public async Task DeleteAsync(Guid id)
{
var entity = await _context.Memories.FindAsync(id);
if (entity != null)
{
_context.Memories.Remove(entity);
await _context.SaveChangesAsync();
}
}

public IQueryable<Memory> Get()
{
return _context.Memories.AsQueryable();
}

public async Task<List<Memory>> GetAllAsync()
{
return await _context.Memories.ToListAsync();
}

public async Task UpdateAsync(Memory entity)
{
_context.Memories.Update(entity);
await _context.SaveChangesAsync();
}
}
public class MemoryRepository : IRepository<Memory>
{
private readonly MeminisseDbContext _context;

public MemoryRepository(MeminisseDbContext context)
{
_context = context;
}

public async Task AddAsync(Memory entity)
{
await _context.Memories.AddAsync(entity);
await _context.SaveChangesAsync();
}

public async Task AddRangeAsync(List<Memory> entities)
{
await _context.Memories.AddRangeAsync(entities);
await _context.SaveChangesAsync();
}

public async Task DeleteAsync(Memory entity)
{
_context.Memories.Remove(entity);
await _context.SaveChangesAsync();
}

public async Task DeleteAsync(Guid id)
{
var entity = await _context.Memories.FindAsync(id);
if (entity != null)
{
_context.Memories.Remove(entity);
await _context.SaveChangesAsync();
}
}

public IQueryable<Memory> Get()
{
return _context.Memories.AsQueryable();
}

public async Task<List<Memory>> GetAllAsync()
{
return await _context.Memories.ToListAsync();
}

public async Task UpdateAsync(Memory entity)
{
_context.Memories.Update(entity);
await _context.SaveChangesAsync();
}
}
public class PersonRepository : IRepository<Person>
{
private readonly MeminisseDbContext _context;

public PersonRepository(MeminisseDbContext context)
{
_context = context;
}

public async Task AddAsync(Person entity)
{
await _context.People.AddAsync(entity);
await _context.SaveChangesAsync();
}

public async Task AddRangeAsync(List<Person> entities)
{
await _context.People.AddRangeAsync(entities);
await _context.SaveChangesAsync();
}

public async Task DeleteAsync(Person entity)
{
_context.People.Remove(entity);
await _context.SaveChangesAsync();
}

public async Task DeleteAsync(Guid id)
{
var entity = await _context.People.FindAsync(id);
if (entity != null)
{
_context.People.Remove(entity);
await _context.SaveChangesAsync();
}
}

public IQueryable<Person> Get()
{
return _context.People.AsQueryable();
}

public async Task<List<Person>> GetAllAsync()
{
return await _context.People.ToListAsync();
}

public async Task UpdateAsync(Person entity)
{
_context.People.Update(entity);
await _context.SaveChangesAsync();
}
}
public class PersonRepository : IRepository<Person>
{
private readonly MeminisseDbContext _context;

public PersonRepository(MeminisseDbContext context)
{
_context = context;
}

public async Task AddAsync(Person entity)
{
await _context.People.AddAsync(entity);
await _context.SaveChangesAsync();
}

public async Task AddRangeAsync(List<Person> entities)
{
await _context.People.AddRangeAsync(entities);
await _context.SaveChangesAsync();
}

public async Task DeleteAsync(Person entity)
{
_context.People.Remove(entity);
await _context.SaveChangesAsync();
}

public async Task DeleteAsync(Guid id)
{
var entity = await _context.People.FindAsync(id);
if (entity != null)
{
_context.People.Remove(entity);
await _context.SaveChangesAsync();
}
}

public IQueryable<Person> Get()
{
return _context.People.AsQueryable();
}

public async Task<List<Person>> GetAllAsync()
{
return await _context.People.ToListAsync();
}

public async Task UpdateAsync(Person entity)
{
_context.People.Update(entity);
await _context.SaveChangesAsync();
}
}
wait first one is wrong had some code that i was testing i updated it to the proper one
Present Chozo
Present Chozo2mo ago
Yeah, I'm stumped, unfortunately. I'd have to dig deeper into your code and DB to troubleshoot this.
Gax
GaxOP2mo ago
its alright, thanks for trying anyways :)
asdf
asdf2mo ago
Memory type already exists in the standard library so you might want to rename it
Gax
GaxOP2mo ago
That is not the issue here though, but I get what you mean
hAssAnNaWaZi
hAssAnNaWaZi5w ago
did you got it? @Gax
Gax
GaxOP5w ago
sadly no at this point im considering rewriting it from scratch and see if a random change i didn't consider does it

Did you find this page helpful?