C
C#12mo ago
Up

✅ EF Core Relations don't seem to work properly

I'm trying to model a fairly complex set of relations in EntityFramework core / ASP.NET, but am failing to create a new entry as the system insists it already exists; yet the corresponding table in the DB is empty
27 Replies
Up
Up12mo ago
[Table("mods")]
public class Mod : BaseModel
{
public string Slug { get; set; } = null!;
public IEnumerable<SupportedPlatform> PlatformIDs { get; set; } = null!;
//...
}
[Table("mods")]
public class Mod : BaseModel
{
public string Slug { get; set; } = null!;
public IEnumerable<SupportedPlatform> PlatformIDs { get; set; } = null!;
//...
}
[Table("mods_supported_platforms")]
public class SupportedPlatform : BaseModel
{
[Required]
public Platform Platform { get; set; } = null!;

[Required]
public string Name { get; set; } = null!;

public Guid PlatformId { get; set; }
}
[Table("mods_supported_platforms")]
public class SupportedPlatform : BaseModel
{
[Required]
public Platform Platform { get; set; } = null!;

[Required]
public string Name { get; set; } = null!;

public Guid PlatformId { get; set; }
}
[Table("platforms")]
public class Platform : BaseModel
{
[Required]
public string Slug { get; set; } = null!;

[Required]
public string Name { get; set; } = null!;
}
[Table("platforms")]
public class Platform : BaseModel
{
[Required]
public string Slug { get; set; } = null!;

[Required]
public string Name { get; set; } = null!;
}
relevant part of my dbcontext init:
modelBuilder.Entity<Mod>().HasIndex(it => it.Slug).IsUnique();
modelBuilder.Entity<Mod>().HasMany(it => it.PlatformIDs);
modelBuilder.Entity<SupportedPlatform>().HasOne(it => it.Platform).WithMany().HasForeignKey(it => it.PlatformId);
modelBuilder.Entity<SupportedPlatform>().HasKey(it => new {it.PlatformId, it.Name});
modelBuilder.Entity<Mod>().HasIndex(it => it.Slug).IsUnique();
modelBuilder.Entity<Mod>().HasMany(it => it.PlatformIDs);
modelBuilder.Entity<SupportedPlatform>().HasOne(it => it.Platform).WithMany().HasForeignKey(it => it.PlatformId);
modelBuilder.Entity<SupportedPlatform>().HasKey(it => new {it.PlatformId, it.Name});
and finally how I'm trying to create a new entry:
public async Task<Mod> AddMod(AddModsInput mod)
{
var platformsStore = _dbContext.Platforms.ToList();
var platforms = mod.SupportedPlatforms.Select(it => new SupportedPlatform()
{
Platform = platformsStore.FirstOrDefault(pl => pl.Slug == it.Platform) ?? throw new KeyNotFoundException(),
Name = it.Id,
});

var result = new Mod
{
Slug = mod.Slug,
PlatformIDs = platforms,
};

_dbContext.Mods.Update(result);
await _dbContext.SaveChangesAsync();
return result;
}
public async Task<Mod> AddMod(AddModsInput mod)
{
var platformsStore = _dbContext.Platforms.ToList();
var platforms = mod.SupportedPlatforms.Select(it => new SupportedPlatform()
{
Platform = platformsStore.FirstOrDefault(pl => pl.Slug == it.Platform) ?? throw new KeyNotFoundException(),
Name = it.Id,
});

var result = new Mod
{
Slug = mod.Slug,
PlatformIDs = platforms,
};

_dbContext.Mods.Update(result);
await _dbContext.SaveChangesAsync();
return result;
}
error message is
Action Exception | Type: 'InvalidOperationException', Message: 'The instance of entity type 'SupportedPlatform' cannot be tracked because another instance with the key value '{PlatformId: <correct GUID>
ab, Name: <correct name>}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.'
Action Exception | Type: 'InvalidOperationException', Message: 'The instance of entity type 'SupportedPlatform' cannot be tracked because another instance with the key value '{PlatformId: <correct GUID>
ab, Name: <correct name>}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.'
undisputed world champions
are you trying to update or insert? _dbContext.Mods.Update(result); is sugesting you trying to update. you should use a Mod that you read from the _dbContext first to update, instead of newing up a new instance
Up
Up12mo ago
upsert. if the entity exists -> update it, if not -> create update() works just fine for that by detecting whether the primary key exists or not
undisputed world champions
tbh i havent used it like that before but the error message complains about SupportedPlatform which you new up too have you tried useing the ones you read from db? maybe it doesnt work that well for naviagation properties? catshrug
Up
Up12mo ago
there are none in the DB I am trying to insert them in this query
undisputed world champions
and i guess the platforms are unique too? so platforms doesn't just contain duplicates?
Up
Up12mo ago
platforms are unique and only contains the correct keys, yes. SupportedPlatform is essentially an IDictionary<Platform, string> property on Mod ..which means serializing it into a separate table
jcotton42
jcotton4212mo ago
Why are you using IEnumerable?
jcotton42
jcotton4212mo ago
Conventions for relationship discovery - EF Core
How navigations, foreign keys, and other aspects of relationships are discovered by EF Core model building conventions
Up
Up12mo ago
what else would I use?
jcotton42
jcotton4212mo ago
List<T> IEnumerable is not appropriate for representing a database relationship
Up
Up12mo ago
well it never complained about it 😅
jcotton42
jcotton4212mo ago
How are you supposed to add or remove from it?
Up
Up12mo ago
I haven't gotten around to that yet, haha
jcotton42
jcotton4212mo ago
Well you'll quickly discover you can't do that with IEnumerable 😛
Up
Up12mo ago
ik but more like, I haven't needed to add/remove yet so how do I use conventions to add constraints? because that is what I've mainly been using the fluent api for
jcotton42
jcotton4212mo ago
You explicitly configure what conventions don't suffice
Up
Up12mo ago
okay, I've redone the DB and am now able to insert data properly. however, I am unable to retrieve the supported platforms it seems
Up
Up12mo ago
Up
Up12mo ago
[Table("mods")]
public class Mod : BaseModel
{
public string Slug { get; set; } = null!;

public ModMetadata Meta { get; set; } = null!;

public ICollection<ModSupportedPlatform> PlatformIDs { get; set; } = new List<ModSupportedPlatform>();
public ICollection<McVersion> SupportedVersions { get; set; } = new List<McVersion>();

[GraphField("supportedPlatforms")]
public IEnumerable<Platform> GetSupportedPlatforms()
{
return PlatformIDs.Select(it => it.Platform).AsEnumerable();
}
}
[Table("mods")]
public class Mod : BaseModel
{
public string Slug { get; set; } = null!;

public ModMetadata Meta { get; set; } = null!;

public ICollection<ModSupportedPlatform> PlatformIDs { get; set; } = new List<ModSupportedPlatform>();
public ICollection<McVersion> SupportedVersions { get; set; } = new List<McVersion>();

[GraphField("supportedPlatforms")]
public IEnumerable<Platform> GetSupportedPlatforms()
{
return PlatformIDs.Select(it => it.Platform).AsEnumerable();
}
}
Up
Up12mo ago
but the data does exist in the DB
Up
Up12mo ago
so looks like the navigation properties aren't being populated thonk
Up
Up12mo ago
here's how I assign those relations:
modelBuilder.Entity<Mod>().HasIndex(it => it.Slug).IsUnique();
modelBuilder.Entity<Mod>().HasOne(it => it.Meta).WithOne(it => it.Mod).HasForeignKey<ModMetadata>(it => it.ModId);
modelBuilder.Entity<Mod>().HasMany(it => it.PlatformIDs).WithOne(it => it.Mod).HasForeignKey(it => it.ModId);
modelBuilder.Entity<ModSupportedPlatform>().HasOne(it => it.Platform).WithMany().HasForeignKey(it => it.PlatformId).IsRequired();
modelBuilder.Entity<ModSupportedPlatform>().HasKey(it => new {it.PlatformId, it.PlatformKey});
modelBuilder.Entity<Mod>().HasIndex(it => it.Slug).IsUnique();
modelBuilder.Entity<Mod>().HasOne(it => it.Meta).WithOne(it => it.Mod).HasForeignKey<ModMetadata>(it => it.ModId);
modelBuilder.Entity<Mod>().HasMany(it => it.PlatformIDs).WithOne(it => it.Mod).HasForeignKey(it => it.ModId);
modelBuilder.Entity<ModSupportedPlatform>().HasOne(it => it.Platform).WithMany().HasForeignKey(it => it.PlatformId).IsRequired();
modelBuilder.Entity<ModSupportedPlatform>().HasKey(it => new {it.PlatformId, it.PlatformKey});
jcotton42
jcotton4212mo ago
Try letting EF infer by convention Don't use configuration unless you need to Also, you made and applied a migration right?
Up
Up12mo ago
yes update: tried it with implicit properties and even deleted + re-seeded the DB. no change
Up
Up12mo ago
also fwiw, when I send the above GQL query, this is the only database query that is executed:
Up
Up11mo ago
any other ideas @jcotton42? update: solution was to enable lazy loading proxies via Microsoft.EntityFrameworkCore.Proxies, and mark all my navigation properties as virtual.